/*
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/*
 *    AlgVector.java
 *    Copyright (C) 2002-2012 University of Waikato, Hamilton, New Zealand
 *
 */

package weka.core;

import java.io.Serializable;
import java.util.Random;

/**
 * Class for performing operations on an algebraic vector of floating-point
 * values.
 *
 * @author Gabi Schmidberger (gabi@cs.waikato.ac.nz)
 * @version $Revision$
 */
public class AlgVector implements Cloneable, Serializable {

    /** for serialization */
    private static final long serialVersionUID = -4023736016850256591L;

    /** The values of the matrix */
    protected double[] m_Elements;

    /**
     * Constructs a vector and initializes it with default values.
     *
     * @param n the number of elements
     */
    public AlgVector(int n) {

        m_Elements = new double[n];
        initialize();
    }

    /**
     * Constructs a vector using a given array.
     *
     * @param array the values of the matrix
     */
    public AlgVector(double[] array) {

        m_Elements = new double[array.length];
        for (int i = 0; i < array.length; i++) {
            m_Elements[i] = array[i];
        }
    }

    /**
     * Constructs a vector using a given data format. The vector has an element for
     * each numerical attribute. The other attributes (nominal, string) are ignored.
     * Random is used to initialize the attributes.
     *
     * @param format the data format to use
     * @param random for initializing the attributes
     * @throws Exception if something goes wrong
     */
    public AlgVector(Instances format, Random random) throws Exception {

        int len = format.numAttributes();
        for (int i = 0; i < format.numAttributes(); i++) {
            if (!format.attribute(i).isNumeric())
                len--;
        }
        if (len > 0) {
            m_Elements = new double[len];
            initialize(random);
        }
    }

    /**
     * Constructs a vector using an instance. The vector has an element for each
     * numerical attribute. The other attributes (nominal, string) are ignored.
     *
     * @param instance with numeric attributes, that AlgVector gets build from
     * @throws Exception if instance doesn't have access to the data format or no
     *                   numeric attributes in the data
     */
    public AlgVector(Instance instance) throws Exception {

        int len = instance.numAttributes();
        for (int i = 0; i < instance.numAttributes(); i++) {
            if (!instance.attribute(i).isNumeric())
                len--;
        }
        if (len > 0) {
            m_Elements = new double[len];
            int n = 0;
            for (int i = 0; i < instance.numAttributes(); i++) {
                if (!instance.attribute(i).isNumeric())
                    continue;
                m_Elements[n] = instance.value(i);
                n++;
            }
        } else {
            throw new IllegalArgumentException("No numeric attributes in data!");
        }
    }

    /**
     * Creates and returns a clone of this object.
     *
     * @return a clone of this instance.
     * @throws CloneNotSupportedException if an error occurs
     */
    public Object clone() throws CloneNotSupportedException {

        AlgVector v = (AlgVector) super.clone();
        v.m_Elements = new double[numElements()];
        for (int i = 0; i < numElements(); i++) {
            v.m_Elements[i] = m_Elements[i];
        }

        return v;
    }

    /**
     * Resets the elements to the default value which is 0.0.
     */
    protected void initialize() {

        for (int i = 0; i < m_Elements.length; i++) {
            m_Elements[i] = 0.0;
        }
    }

    /**
     * Initializes the values with random numbers between 0 and 1.
     * 
     * @param random the random number generator to use for initializing
     */
    protected void initialize(Random random) {

        for (int i = 0; i < m_Elements.length; i++) {
            m_Elements[i] = random.nextDouble();
        }
    }

    /**
     * Returns the value of a cell in the matrix.
     *
     * @param index the row's index
     * @return the value of the cell of the vector
     */
    public final double getElement(int index) {
        return m_Elements[index];
    }

    /**
     * Returns the number of elements in the vector.
     *
     * @return the number of rows
     */
    public final int numElements() {

        return m_Elements.length;
    }

    /**
     * Sets an element of the matrix to the given value.
     *
     * @param index the elements index
     * @param value the new value
     */
    public final void setElement(int index, double value) {

        m_Elements[index] = value;
    }

    /**
     * Sets the elements of the vector to values of the given array. Performs a deep
     * copy.
     *
     * @param elements an array of doubles
     */
    public final void setElements(double[] elements) {

        for (int i = 0; i < elements.length; i++) {
            m_Elements[i] = elements[i];
        }
    }

    /**
     * Gets the elements of the vector and returns them as double array.
     *
     * @return an array of doubles
     */
    public double[] getElements() {

        double[] elements = new double[this.numElements()];
        for (int i = 0; i < elements.length; i++) {
            elements[i] = m_Elements[i];
        }
        return elements;
    }

    /**
     * Gets the elements of the vector as an instance. !! NON-numeric data is
     * ignored sofar
     * 
     * @param model  the dataset structure to fit the data to
     * @param random in case of nominal values a random label is taken
     * @return an array of doubles
     * @throws Exception if length of vector is not number of numerical attributes
     */
    public Instance getAsInstance(Instances model, Random random) throws Exception {

        Instance newInst = null;

        if (m_Elements != null) {
            newInst = new DenseInstance(model.numAttributes());
            newInst.setDataset(model);

            for (int i = 0, j = 0; i < model.numAttributes(); i++) {
                if (model.attribute(i).isNumeric()) {
                    if (j >= m_Elements.length)
                        throw new Exception("Datatypes are not compatible.");
                    newInst.setValue(i, m_Elements[j++]);
                }
                if (model.attribute(i).isNominal()) {
                    int newVal = (int) (random.nextDouble() * (double) (model.attribute(i).numValues()));
                    if (newVal == (int) model.attribute(i).numValues())
                        newVal -= 1;
                    newInst.setValue(i, newVal);
                }
            }
        }
        return newInst;
    }

    /**
     * Returns the sum of this vector with another.
     *
     * @param other the vector to add
     * @return a vector containing the sum.
     */
    public final AlgVector add(AlgVector other) {

        AlgVector b = null;

        if (m_Elements != null) {
            int n = m_Elements.length;
            try {
                b = (AlgVector) clone();
            } catch (CloneNotSupportedException ex) {
                b = new AlgVector(n);
            }

            for (int i = 0; i < n; i++) {
                b.m_Elements[i] = m_Elements[i] + other.m_Elements[i];
            }
        }

        return b;
    }

    /**
     * Returns the difference of this vector minus another.
     *
     * @param other the vector to subtract
     * @return a vector containing the difference vector.
     */
    public final AlgVector substract(AlgVector other) {

        int n = m_Elements.length;
        AlgVector b;
        try {
            b = (AlgVector) clone();
        } catch (CloneNotSupportedException ex) {
            b = new AlgVector(n);
        }

        for (int i = 0; i < n; i++) {
            b.m_Elements[i] = m_Elements[i] - other.m_Elements[i];
        }

        return b;
    }

    /**
     * Returns the inner (or dot) product of two vectors
     *
     * @param b the multiplication matrix
     * @return the double representing the dot product
     */
    public final double dotMultiply(AlgVector b) {

        double sum = 0.0;

        if (m_Elements != null) {
            int n = m_Elements.length;

            for (int i = 0; i < n; i++) {
                sum += m_Elements[i] * b.m_Elements[i];
            }
        }

        return sum;
    }

    /**
     * Computes the scalar product of this vector with a scalar
     *
     * @param s the scalar
     */
    public final void scalarMultiply(double s) {

        if (m_Elements != null) {
            int n = m_Elements.length;

            for (int i = 0; i < n; i++) {
                m_Elements[i] = s * m_Elements[i];
            }
        }
    }

    /**
     * Changes the length of a vector.
     *
     * @param len the new length of the vector
     */
    public void changeLength(double len) {

        double factor = this.norm();
        factor = len / factor;
        scalarMultiply(factor);
    }

    /**
     * Returns the norm of the vector
     *
     * @return the norm of the vector
     */
    public double norm() {

        if (m_Elements != null) {
            int n = m_Elements.length;
            double sum = 0.0;

            for (int i = 0; i < n; i++) {
                sum += m_Elements[i] * m_Elements[i];
            }
            return Math.pow(sum, 0.5);
        } else
            return 0.0;
    }

    /**
     * Norms this vector to length 1.0
     */
    public final void normVector() {

        double len = this.norm();
        this.scalarMultiply(1 / len);
    }

    /**
     * Converts a vector to a string
     *
     * @return the converted string
     */
    public String toString() {

        StringBuffer text = new StringBuffer();
        for (int i = 0; i < m_Elements.length; i++) {
            if (i > 0)
                text.append(",");
            text.append(Utils.doubleToString(m_Elements[i], 6));
        }

        text.append("\n");
        return text.toString();
    }

    /**
     * Main method for testing this class, can take an ARFF file as first argument.
     * 
     * @param args commandline options
     * @throws Exception if something goes wrong in testing
     */
    public static void main(String[] args) throws Exception {

        double[] first = { 2.3, 1.2, 5.0 };

        try {
            AlgVector test = new AlgVector(first);
            System.out.println("test:\n " + test);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
